package ch.sv7.tool.dbmanager.db.fixbuilder;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import ch.sv7.tool.dbmanager.Context;
import ch.sv7.tool.dbmanager.db.ColumnSchema;
import ch.sv7.tool.dbmanager.db.ConstraintSchema;
import ch.sv7.tool.dbmanager.db.ForeignKeySchema;
import ch.sv7.tool.dbmanager.db.IndexSchema;
import ch.sv7.tool.dbmanager.db.SchemaElement;
import ch.sv7.tool.dbmanager.db.dialect.CustomTypeConversion;
import ch.sv7.tool.dbmanager.db.dialect.Dialect;
import ch.sv7.tool.dbmanager.db.difference.AbstractColumnDifference;
import ch.sv7.tool.dbmanager.db.difference.SchemaDifference;
import ch.sv7.tool.dbmanager.db.fix.CopyColumnFix;
import ch.sv7.tool.dbmanager.db.fix.CopyCustomConvertColumnFix;
import ch.sv7.tool.dbmanager.db.fix.CreateColumnFix;
import ch.sv7.tool.dbmanager.db.fix.CreateConstraintFix;
import ch.sv7.tool.dbmanager.db.fix.CreateForeignKeyFix;
import ch.sv7.tool.dbmanager.db.fix.CreateIndexFix;
import ch.sv7.tool.dbmanager.db.fix.DropConstraintFix;
import ch.sv7.tool.dbmanager.db.fix.DropForeignKeyFix;
import ch.sv7.tool.dbmanager.db.fix.DropIndexFix;
import ch.sv7.tool.dbmanager.db.fix.GroupedSchemaFix;
import ch.sv7.tool.dbmanager.db.fix.ImplicitUpdateColumnFix;
import ch.sv7.tool.dbmanager.db.fix.RenameColumnFix;
import ch.sv7.tool.dbmanager.db.fix.SchemaFix;
import ch.sv7.tool.log.Logger;

/**
 * TODO: Document me!
 *
 * @author svt
 *
 */
public class ColumnTypeFixBuilder implements FilterFixBuilder {

	private static final Logger log = Logger.getLogger(ColumnTypeFixBuilder.class);
	
	private Set indexes = new HashSet();
	private Set foreignKeys = new HashSet();
	private boolean implicitChange;
	
	public boolean doAutoIncremenentNeedColumnCopy(Dialect dialect, Context context, AbstractColumnDifference columnDifference){
		return columnDifference.getActualColumnSchema().getType().isAutoIncrement() !=
			columnDifference.getExpectedColumnSchema().getType().isAutoIncrement();
	}
	
	/**
	 * @return the implicitChange
	 */
	public boolean isImplicitChange() {
		return implicitChange;
	}

	/**
	 * @param implicitChange the implicitChange to set
	 */
	public void setImplicitChange(boolean implicitChange) {
		this.implicitChange = implicitChange;
	}

	public boolean doNullableNeedColumnCopy(Dialect dialect, Context context, AbstractColumnDifference columnDifference){
		if (columnDifference.getActualColumnSchema().getType().isNullable() !=
			columnDifference.getExpectedColumnSchema().getType().isNullable()){
			// Nullable field change
			if (columnDifference.getExpectedColumnSchema().getType().isNullable()){
				// Target is nullable
				return false;
			} else {
				if (!columnDifference.getExpectedColumnSchema().getType().hasDefaultValue()){
					return true;
				}
			}
		}
		return false;
	}
	
	public boolean doDefaultValueNeedColumnCopy(Dialect dialect, Context context, AbstractColumnDifference columnDifference){
		if (columnDifference.getActualColumnSchema().getType().hasDefaultValue() && 
				columnDifference.getExpectedColumnSchema().getType().hasDefaultValue() &&
				!columnDifference.getActualColumnSchema().getType().getDefaultValue()
					.equals(columnDifference.getExpectedColumnSchema().getType().getDefaultValue())){
			// Default value change
			return true;
		}
		
		if (columnDifference.getActualColumnSchema().getType().hasDefaultValue() !=
			columnDifference.getExpectedColumnSchema().getType().hasDefaultValue()){
			// Add or remove default value
			return true;
		}
		return false;
	}
	
	public boolean doConversionTypeNeedColumnCopy(Dialect dialect, Context context, AbstractColumnDifference columnDifference){
		return Dialect.CONVERSION_TYPE_IMPLICIT != 
			dialect.getDataTypeConversion(
					columnDifference.getActualColumnSchema().getType().getTypeName(), 
					columnDifference.getExpectedColumnSchema().getType().getTypeName())
			&& Dialect.CONVERSION_TYPE_SAME != 
				dialect.getDataTypeConversion(
						columnDifference.getActualColumnSchema().getType().getTypeName(), 
						columnDifference.getExpectedColumnSchema().getType().getTypeName());
	}
	
	public SchemaFix[] createFixes(SchemaDifference difference, Dialect dialect, Context context) {
		AbstractColumnDifference columnDifference = (AbstractColumnDifference)difference;
		boolean autoIncremenentNeedColumnCopy = doAutoIncremenentNeedColumnCopy(dialect, context, columnDifference);
		boolean nullableNeedColumnCopy = doNullableNeedColumnCopy(dialect, context, columnDifference);
		boolean defaultValueNeedColumnCopy = doDefaultValueNeedColumnCopy(dialect, context, columnDifference);
		boolean conversionTypeNeedColumnCopy = doConversionTypeNeedColumnCopy(dialect, context, columnDifference);
		
		if (!conversionTypeNeedColumnCopy 
				&& !autoIncremenentNeedColumnCopy
				&& !nullableNeedColumnCopy
				&& !defaultValueNeedColumnCopy){
			List fixes = new ArrayList();

			setImplicitChange(true);
			/*
			 * Drop indexes if needed
			 */
			Iterator oldIndexIter = getIndexesToDrop(columnDifference);
			while (oldIndexIter.hasNext()) {
				IndexSchema index = (IndexSchema)oldIndexIter.next();
				indexes.add(((SchemaElement)index).getSchemaIdentifier());
				fixes.add(new DropIndexFix(index));
			}
			Iterator newIndexIter = getIndexesToCreate(columnDifference);
			while (newIndexIter.hasNext()) {
				IndexSchema index = (IndexSchema)newIndexIter.next();
				indexes.add(((SchemaElement)index).getSchemaIdentifier());
				fixes.add(new CreateIndexFix(index));
			}		
			Iterator oldFkIter = getForeignKeyToDrop(columnDifference);
			while (oldFkIter.hasNext()) {
				ForeignKeySchema fk = (ForeignKeySchema)oldFkIter.next();
				foreignKeys.add(((SchemaElement)fk).getSchemaIdentifier());
				fixes.add(new DropForeignKeyFix(fk));
			}
			Iterator newFkIter = getForeignKeyToCreate(columnDifference);
			while (newFkIter.hasNext()) {
				ForeignKeySchema fk = (ForeignKeySchema)newFkIter.next();
				foreignKeys.add(((SchemaElement)fk).getSchemaIdentifier());
				fixes.add(new CreateForeignKeyFix(fk));
			}			
			/*
			 * Implicit conversion
			 */
			ImplicitUpdateColumnFix fix = new ImplicitUpdateColumnFix(columnDifference.getExpectedColumnSchema());
			
			/*
			 * Check if column becomes not-null
			 */
			if (columnDifference.getActualColumnSchema().getType().isNullable() &&
					!columnDifference.getExpectedColumnSchema().getType().isNullable()
					&& columnDifference.getExpectedColumnSchema().getType().hasDefaultValue()){
				fix.setFixNullValues(true);
			}
			
			fixes.add(fix);

			return (SchemaFix[])fixes.toArray(new SchemaFix[fixes.size()]);			
		} else {
			
			setImplicitChange(false);
			/*
			 * Explicit conversion
			 */
			List fixes = new ArrayList();
			/*
			 * Drop and create all inherent index and fk
			 */
			Iterator oldIndexIter = getIndexesToDrop(columnDifference);
			while (oldIndexIter.hasNext()) {
				IndexSchema index = (IndexSchema)oldIndexIter.next();
				indexes.add(((SchemaElement)index).getSchemaIdentifier());
				fixes.add(new DropIndexFix(index));

			}
			Iterator newIndexIter = getIndexesToCreate(columnDifference);
			while (newIndexIter.hasNext()) {
				IndexSchema index = (IndexSchema)newIndexIter.next();
				indexes.add(((SchemaElement)index).getSchemaIdentifier());
				fixes.add(new CreateIndexFix(index));

			}
			Iterator oldFkIter = getForeignKeyToDrop(columnDifference);
			while (oldFkIter.hasNext()) {
				ForeignKeySchema fk = (ForeignKeySchema)oldFkIter.next();
				foreignKeys.add(((SchemaElement)fk).getSchemaIdentifier());
				fixes.add(new DropForeignKeyFix(fk));
			}
			Iterator newFkIter = getForeignKeyToCreate(columnDifference);
			while (newFkIter.hasNext()) {
				ForeignKeySchema fk = (ForeignKeySchema)newFkIter.next();
				foreignKeys.add(((SchemaElement)fk).getSchemaIdentifier());
				fixes.add(new CreateForeignKeyFix(fk));
			}
			
			/*
			 * Drop and create all constraints (we do no known which applies to this column)
			 */
			Iterator oldConsIter = columnDifference.getActualColumnSchema().getTable().getConstraints().iterator();
			while (oldConsIter.hasNext()) {
				ConstraintSchema cs = (ConstraintSchema)oldConsIter.next();
				fixes.add(new DropConstraintFix(cs));
			}
			Iterator newConsIter = columnDifference.getExpectedColumnSchema().getTable().getConstraints().iterator();
			while (newConsIter.hasNext()) {
				ConstraintSchema cs = (ConstraintSchema)newConsIter.next();
				fixes.add(new CreateConstraintFix(cs));
			}
			/*
			 * Create a grouped Fix
			 */
			boolean conversionPossible = true;
			GroupedSchemaFix groupedFix = new GroupedSchemaFix(
					columnDifference.getExpectedColumnSchema(),
					SchemaFix.ACTION_EDIT, 
					SchemaFix.PRIORITY_COLUMN_EXPLICIT_UPDATE, 
					"Explicit Update column " 
						+ columnDifference.getActualColumnSchema().getName() 
						+ " to table " 
						+ columnDifference.getActualColumnSchema().getTable().getName());
			
			/*
			 * Create new _$temp column
			 */
			String tempNewName = columnDifference.getExpectedColumnSchema().getName() + "_$temp";
			tempNewName = validateNotUsedColumnName(columnDifference.getActualColumnSchema(), tempNewName);
			ColumnSchema expectedTempColumn = columnDifference.getExpectedColumnSchema().cloneColumn();

			if (!columnDifference.getExpectedColumnSchema().getType().hasDefaultValue() 
					&& !columnDifference.getExpectedColumnSchema().getType().isNullable()
					&& !columnDifference.getExpectedColumnSchema().getType().isAutoIncrement()){
				expectedTempColumn.getType().setNullable(true);
			}
			expectedTempColumn.setName(tempNewName);
			
			groupedFix.addFix(new CreateColumnFix(expectedTempColumn));
			
			/*
			 * Convert old column data to new 
			 */
			
			if (Dialect.CONVERSION_TYPE_EXPLICIT == 
					dialect.getDataTypeConversion(
							columnDifference.getActualColumnSchema().getType().getTypeName(), 
							columnDifference.getExpectedColumnSchema().getType().getTypeName())
				||
				Dialect.CONVERSION_TYPE_IMPLICIT == 
					dialect.getDataTypeConversion(
							columnDifference.getActualColumnSchema().getType().getTypeName(), 
							columnDifference.getExpectedColumnSchema().getType().getTypeName())
				||
				Dialect.CONVERSION_TYPE_SAME == 
					dialect.getDataTypeConversion(
							columnDifference.getActualColumnSchema().getType().getTypeName(), 
							columnDifference.getExpectedColumnSchema().getType().getTypeName())){
				
				
				CopyColumnFix fix = new CopyColumnFix(columnDifference.getActualColumnSchema(), expectedTempColumn);
				if (columnDifference.getExpectedColumnSchema().getType().isAutoIncrement()){
					fix.setAutoIncrement(true);
				}
				groupedFix.addFixes(getPreCopyFixes(columnDifference));
				groupedFix.addFix(fix);
				groupedFix.addFixes(getPostCopyFixes(columnDifference));
			} else if (Dialect.CONVERSION_TYPE_CUSTOM == 
					dialect.getDataTypeConversion(
							columnDifference.getActualColumnSchema().getType().getTypeName(), 
							columnDifference.getExpectedColumnSchema().getType().getTypeName())){
			
				CustomTypeConversion converter = context.getConverter(
						columnDifference.getActualColumnSchema().getType().getTypeName(), 
						columnDifference.getExpectedColumnSchema().getType().getTypeName());
				
				if (converter != null && columnDifference.getActualColumnSchema().getTable().getPrimaryKeyColumnSchemas().length > 0) {
					
					SchemaFix fix = new CopyCustomConvertColumnFix(columnDifference.getActualColumnSchema(), expectedTempColumn, converter);
					groupedFix.addFixes(getPreCopyFixes(columnDifference));
					groupedFix.addFix(fix);
					groupedFix.addFixes(getPostCopyFixes(columnDifference));
				} else if (converter != null && columnDifference.getActualColumnSchema().getTable().getPrimaryKeyColumnSchemas().length == 0) {
					conversionPossible = false;
					log.error("Unable to perform a type conversion on column "
							+ columnDifference.getActualColumnSchema().getName()
							+" because table has no primary keys : " 
							+ columnDifference.getActualColumnSchema().getTable().getName());
				} else {
					conversionPossible = false;
					log.error("No converter found for conversion from " 
							+ columnDifference.getActualColumnSchema().getType().getTypeName()
							+ " to "
							+ columnDifference.getExpectedColumnSchema().getType().getTypeName()
							+ " for " + columnDifference.getTableName()
							+ "." + columnDifference.getExpectedColumnSchema().getName());
				}
			} else {
				conversionPossible = false;
				log.error("Bad conversion type " + columnDifference.getActualColumnSchema().getType().getTypeName() + " -> "
							+ columnDifference.getExpectedColumnSchema().getType().getTypeName() + "  conversion:" 
							+ dialect.getDataTypeConversion(
							columnDifference.getActualColumnSchema().getType().getTypeName(), 
							columnDifference.getExpectedColumnSchema().getType().getTypeName()));
			}

			/*
			 * Rename actual column to ..._$nu
			 */
			ColumnSchema actualSchema = columnDifference.getActualColumnSchema().cloneColumn();
			actualSchema.getType().setNullable(true);
			ImplicitUpdateColumnFix fixNullable = new ImplicitUpdateColumnFix(actualSchema);
			groupedFix.addFix(fixNullable);
			String notUsedName = actualSchema.getName() + "_$nu";
			notUsedName = validateNotUsedColumnName(actualSchema, notUsedName);
			groupedFix.addFix(new RenameColumnFix(actualSchema, notUsedName));

			/*
			 * Rename _$temp column to final column
			 */
			groupedFix.addFix(new RenameColumnFix(expectedTempColumn, columnDifference.getExpectedColumnSchema().getName()));
			
			if (conversionPossible) {
				fixes.add(groupedFix);
			}
			return (SchemaFix[])fixes.toArray(new SchemaFix[fixes.size()]);
		}
	}

	public Iterator getIndexesToDrop(AbstractColumnDifference columnDifference) {
		if (isImplicitChange()){
			return new ArrayList().iterator();
		} else {
			return columnDifference.getActualColumnSchema().getReferencedIndexes().iterator();
		}
	}
	
	public Iterator getIndexesToCreate(AbstractColumnDifference columnDifference) {
		if (isImplicitChange()) {
			return new ArrayList().iterator();		
		} else {
			return columnDifference.getExpectedColumnSchema().getReferencedIndexes().iterator();
		}
	}
	
	public Iterator getForeignKeyToDrop(AbstractColumnDifference columnDifference) {
		if (isImplicitChange()){
			return new ArrayList().iterator();
		} else {
			return columnDifference.getActualColumnSchema().getReferencedForeignKeys().iterator();
		}
	}
	
	public Iterator getForeignKeyToCreate(AbstractColumnDifference columnDifference) {
		if (isImplicitChange()) {
			return new ArrayList().iterator();		
		} else {
			return columnDifference.getExpectedColumnSchema().getReferencedForeignKeys().iterator();
		}
	}
	
	public List getPreCopyFixes(AbstractColumnDifference difference) {
		return new ArrayList();
	}
	
	public List getPostCopyFixes(AbstractColumnDifference difference) {
		return new ArrayList();
	}
	
	
	public boolean ignoreFix(SchemaFix fix) {
		String identifier = fix.getActionTarget().getSchemaIdentifier();
		/*
		 * Remove all editions on index that will be dropped
		 */
		if (indexes.contains(identifier) && fix.getAction() == SchemaFix.ACTION_EDIT){
			return true;
		}	
		/*
		 * Remove all editions on foreign keys that will be dropped
		 */
		if (foreignKeys.contains(identifier) && fix.getAction() == SchemaFix.ACTION_EDIT){
			return true;
		}	
		
		return false;
	}

	protected String validateNotUsedColumnName(ColumnSchema columnSchema, String newName) {
		if (columnSchema.getTable().getColumnSchema(newName) == null) {
			return newName;
		} else {
			int count = 1;
			ColumnSchema existingCol = null;
			String suggestedName = null;
			do {
				suggestedName = newName + Integer.toString(count++);
				existingCol = columnSchema.getTable().getColumnSchema(suggestedName);
			} while (existingCol != null);
			return suggestedName;
		}
	}

}
